﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Sworm
{
    enum JoinType
    {
        Left,
        Inner,
    }

    sealed class ResultColumnDefinition
    {
        public ColumnData Column { get; set; }
        public string KnownName { get; set; }
        public string ResultSetName { get { return KnownName ?? Column.Name.Unescaped; } }
        public int Index { get; set; }
    }

    sealed class JoinDefinition
    {
        private List<ResultColumnDefinition> columns = new List<ResultColumnDefinition>();
        public IEnumerable<ResultColumnDefinition> Columns { get { return columns; } }
        private ResultColumnDefinition pk;
        public ResultColumnDefinition PrimaryKey { get { return pk; } }

        // This is nasty... we want to:
        // 1) add all known columns (see AddKnownColumn(...))
        // 2) put in "guesses" for columns the user may have added
        // 3) figure out what's actually in the data reader and trim the collection
        internal void Prepare(DataReader reader)
        {
            foreach (var column in Table.Columns)
            {
                if (columns.Any(c => c.Column == column))
                {
                    continue;
                }
                columns.Add(new ResultColumnDefinition() { Column = column });
            }

            for (int i = columns.Count - 1; i >= 0; i--)
            {
                if (!reader.Contains(columns[i].ResultSetName))
                {
                    columns.RemoveAt(i);
                }
                else
                {
                    columns[i].Index = reader.GetIndex(columns[i].ResultSetName);
                }
            }

            pk = columns.Single(c => c.Column == Table.OptionalData.PrimaryKey);
        }

        public void AddKnownColumn(ColumnData column, SqlIdentifier identifier)
        {
            columns.Add(new ResultColumnDefinition() { Column = column, KnownName = identifier.Unescaped });
        }

        private JoinDefinition()
        {
            Joins = new List<JoinDefinition>();
        }

        public JoinDefinition(TableData root, SqlIdentifier alias)
            : this()
        {
            this.Table = root;
            this.Alias = alias;
        }

        private JoinDefinition(RelationshipData relationship, SqlIdentifier alias, JoinType type)
            : this()
        {
            this.Relationship = relationship;
            this.Table = Relationship.DestinationColumn.Table;
            this.Alias = alias;
            this.JoinType = type;
        }

        public TableData Table { get; private set; }
        public SqlIdentifier Alias { get; private set; }
        public List<JoinDefinition> Joins { get; private set; }

        // these will be null for the root
        public JoinType JoinType { get; private set; }
        public RelationshipData Relationship { get; private set; }
        public JoinDefinition Parent { get; private set; }

        public void AddChild(RelationshipData relationship, SqlIdentifier alias, JoinType type)
        {
            var child = new JoinDefinition(relationship, alias, type);
            child.Parent = this;
            this.Joins.Add(child);
        }
    }

    sealed class JoinResultSetDescription
    {
        public JoinDefinition Root { get; private set; }

        private static IEnumerable<JoinDefinition> JoinsDFS(JoinDefinition current)
        {
            yield return current;
            foreach (var child in current.Joins)
            {
                foreach (var x in JoinsDFS(child))
                {
                    yield return x;
                }
            }
        }

        public IEnumerable<JoinDefinition> JoinsIncludeRoot { get { return JoinsDFS(Root); } }

        public IEnumerable<JoinDefinition> JoinsExcludeRoot { get { return JoinsIncludeRoot.Skip(1); } }

        // call this when the results are known but before trying to start reading
        internal void Prepare(DataReader reader)
        {
            foreach (var join in JoinsIncludeRoot)
            {
                join.Prepare(reader);
            }
        }

        public JoinResultSetDescription(TableData rootTable, SqlIdentifier alias)
        {
            this.Root = new JoinDefinition(rootTable, alias);
        }

        public void AddJoin(RelationshipData relationship, SqlIdentifier alias, JoinType joinType)
        {
            if (JoinsIncludeRoot.Any(j => j.Relationship == relationship))
            {
                throw new InvalidQueryException("relationship is already on the join");
            }

            if (JoinsExcludeRoot.Any(j => IsInverse(j.Relationship, relationship)))
            {
                throw new InvalidQueryException("circular join detected");
            }

            var candidates = JoinsIncludeRoot.Where(j => j.Table == relationship.SourceTable).ToList();
            if (candidates.Count == 0)
            {
                throw new InvalidQueryException(string.Format(
                    "source table {0} is not on the result set", relationship.SourceTable));
            }

            if (candidates.Count == 1)
            {
                candidates.Single().AddChild(relationship, alias, joinType);
                return;
            }

            throw new Exception("TODO need the full join path?");
        }

        public JoinDefinition GetByAliasOrDefault(string alias)
        {
            return JoinsIncludeRoot.SingleOrDefault(j => j.Alias.Unescaped.EqualsIgnoreCase(alias));
        }

        public bool AlreadyContainsAlias(string alias)
        {
            return GetByAliasOrDefault(alias) != null;
        }

        public static bool IsInverse(RelationshipData first, RelationshipData second)
        {
            return first.SourceMember == second.DestinationMember
                && first.DestinationMember == second.SourceMember;
        }
    }
}
